/*
 * Created on Aug 1, 2006
 */
package com.mgmaps.gps;

import java.io.*;
import javax.microedition.io.*;
import com.mgmaps.tools.GeoTools;
import com.mgmaps.tools.StringTools;
import com.mgmaps.utils.ExtLogger;
import com.nutiteq.components.WgsPoint;

/**
 * NMEA parser for bluetooth and serial port receivers.
 *
 * @author CristiS
 * @version 1.0
 */
public abstract class GPSNMEA extends GPSReceiver {

  // constants
  private final static int SLEEP_TIME = 15000;
  // delays for S60 fix
  private final static int VERY_SHORT_SLEEP_TIME = 10;
  private final static int VERY_SHORT_SLEEP_INT = 20;
  private final static int SHORT_SLEEP_TIME_1 = 200;
  private final static int SHORT_SLEEP_TIME_2 = 30;

  protected boolean connected;
  protected Connection connection;
  protected InputStream is;

  // private fields used by readLineV2
  private final byte[] buf = new byte[40];
  private int buflen = 0;
  private int bufpos = 0;
  
  // lon/lat
  protected double lon;
  protected double lat;
  protected double oldLon;
  protected double oldLat;

  // other info
  protected double speed;
  protected double heading;
  protected double altitude;
  protected double precision;
  
  // satellites
  protected int numFixSatellites;
  protected int[] fixSatellites = new int[12];
  protected int numSatellites;
  protected SatelliteData[] satelliteData;
  
  // satellite info
  // protected String satInfo;
  
  /**
   * Constructor for GPSNMEA.
   */
  public GPSNMEA() {
    connected = false;
    active = false;
    
    // not initialized yet
    lat = Double.NaN;
    lon = Double.NaN;
    oldLat = Double.NaN;
    oldLon = Double.NaN;
    speed = Double.NaN;
    heading = Double.NaN;
    altitude = Double.NaN;
    precision = Double.NaN;
    connection = null;
    is = null;
    // satInfo = "";
  }
  
  /**
   * Connect method.
   * 
   * @return true if connected, false otherwise
   */
  public abstract boolean connect();
  
  /**
   * Disconnect - and notify.
   * 
   * @see com.mgmaps.gps.GPSReceiver#disconnect()
   */
  public void disconnect() {
    disconnect(true);
  }

  /**
   * Getter for connected.
   *
   * @return Returns the value of connected.
   */
  public synchronized boolean isConnected() {
    return connected;
  }
  
  /**
   * Disconnect.
   * 
   * @param notifyFlag whether to notify
   */
  public synchronized void disconnect(boolean notifyFlag) {
    try {
      if (is != null)
        is.close();
      if (connection != null)
        connection.close();
    }
    catch (IOException e) {
      ExtLogger.handle(e, false, "Error closing connection");
    }
    is = null;
    connection = null;
    connected = false;
    
    if (notifyFlag)
      notify();
  }

  /**
   * Getter for altitude.
   *
   * @return Returns the value of altitude.
   */
  public double getAltitude() {
    return altitude;
  }

  /**
   * Getter for heading.
   *
   * @return Returns the value of heading.
   */
  public double getHeading() {
    if (speed <= 0)
      return Double.NaN;
    return heading;
  }

  /**
   * Getter for speed.
   *
   * @return Returns the value of speed.
   */
  public double getSpeed() {
    return speed;
  }

  /**
   * Get IDs of satellites that have a fix.
   * @return array of satellites that have a fix
   * @see com.mgmaps.gps.GPSReceiver#getFixSatellites()
   */
  public int[] getFixSatellites() {
    final int[] fixSats = new int[numFixSatellites];
    for (int i=0, j=0; i<fixSatellites.length; i++)
      if (fixSatellites[i] != 0)
        fixSats[j++] = fixSatellites[i];
    return fixSats;
  }
  
  /**
   * Get data for satellites in view.
   * @return data for satellites in view
   * @see com.mgmaps.gps.GPSReceiver#getSatelliteData()
   */
  public SatelliteData[] getSatelliteData() {
    int count = 0;
    for (int i=0; i<satelliteData.length; i++)
      if (satelliteData[i] != null)
        count++;
    final SatelliteData[] sats = new SatelliteData[count];
    for (int i=0; i<satelliteData.length; i++)
      if (satelliteData[i] != null)
        sats[i] = satelliteData[i];
    return sats;
  }
  
  /**
   * Getter for precision.
   * 
   * @return precision
   */
  public double getPrecision() {
    return precision;
  }

  /**
   * Parse a GPS string.
   * 
   * @param gpsString the string to parse
   */
  protected synchronized void parse(String gpsString) {
    // NMEA debugging
    // ExtLogger.debug("NMEA data: " + gpsString);
    
    // do nothing if the string is empty
    if (gpsString.trim().length() == 0)
      return;
    
    try {
      // remove the checksum
      String[] star = StringTools.split(gpsString, '*');
      gpsString = star[0];
    
      String[] tokens = StringTools.split(gpsString, ',');
    
      // parse the command
      String command = tokens[0];
      if (command.equals("$GPRMC"))
        handleGPRMC(tokens);
      else if (command.equals("$GPGGA"))
        handleGPGGA(tokens);
      else if (command.equals("$GPGLL"))
        handleGPGLL(tokens);
      else if (command.equals("$GPVTG"))
        handleGPVTG(tokens);
      else if (command.equals("$GPGSA"))
        handleGPGSA(tokens);
      else if (command.equals("$GPGSV"))
        handleGPGSV(tokens);
      // else if (command.equals("$PGRME"))
      //   handlePGRME(tokens);
      // else if (command.equals("$PGRMZ"))
      //   handlePGRMZ(tokens);      
    }
    catch (Exception ex) {
      ExtLogger.handle(ex, false, "Exception while parsing GPS string: " + gpsString);
    }
  }
  
  /**
   * Process the GPRMC GPS command.
   * @param tokens the tokens of the command
   */
  private void handleGPRMC(String[] tokens) {
    if (tokens.length < 12) // invalid statement
      return;
    if (tokens[2].equals("V")) // invalid
      return;
    
    hasFix = true;
    try {
      fixTime = StringTools.parseTimeGPS(tokens[9], tokens[1]);
      clockDelta = System.currentTimeMillis()-fixTime;
    }
    catch (NumberFormatException nfe) {
      ExtLogger.handle(nfe, false, "GPRMC Error parsing time from GPS");
      fixTime = System.currentTimeMillis();
      clockDelta = 0;
    }
    
    try {
      final double newLat = StringTools.parseCoordGPS(tokens[3], tokens[4]);
      final double newLon = StringTools.parseCoordGPS(tokens[5], tokens[6]);
      lat = newLat;
      lon = newLon;
    
      try {
        double temp = Double.parseDouble(tokens[7]); 
        speed = temp * GeoTools.METERS_PER_HOUR_PER_KNOT / 1000;
      }
      catch (Exception ex) {
        speed = 0;
      }
      try {
        heading = Double.parseDouble(tokens[8]);
      }
      catch (Exception ex) {
        heading = Double.NaN;
      }
    }
    catch (NumberFormatException nfe) {
      ExtLogger.handle(nfe, false, "GPRMC Error parsing coordinates");
    }
  }
  
  /**
   * Process the GPGGA GPS command.
   * @param tokens the tokens of the command
   */
  private void handleGPGGA(String[] tokens) {
    if (tokens.length < 12) // invalid statement
      return;
    if (tokens[6].equals("0")) // invalid
      return;

    // hasFix = true;
    // fixTime = (int) (System.currentTimeMillis()/1000);

    try {
      final double newLat = StringTools.parseCoordGPS(tokens[2], tokens[3]);
      final double newLon = StringTools.parseCoordGPS(tokens[4], tokens[5]);
      lat = newLat;
      lon = newLon;
    
      altitude = Double.parseDouble(tokens[9]);
    }
    catch (NumberFormatException nfe) {
      ExtLogger.handle(nfe, false, "GPGGA Error parsing coordinates");
    }
  }
  
  /**
   * Process the GPGLL GPS command.
   * @param tokens the tokens of the command
   */
  private void handleGPGLL(String[] tokens) {
    if (tokens.length < 7) // invalid statement
      return;
    if (tokens[6].equals("V")) // invalid
      return;

    // hasFix = true;
    // fixTime = (int) (System.currentTimeMillis()/1000);

    try {
      final double newLat = StringTools.parseCoordGPS(tokens[1], tokens[2]);
      final double newLon = StringTools.parseCoordGPS(tokens[3], tokens[4]);
      lat = newLat;
      lon = newLon;
    } 
    catch (NumberFormatException nfe) {
      ExtLogger.handle(nfe, false, "GPGLL Error parsing coordinates");
    }
  }
    
  /**
   * Process the GPVTG GPS command.
   * @param tokens the tokens of the command
   */
  private void handleGPVTG(String[] tokens) {
    if (tokens.length < 9)
      return;
    
    try {
      double temp = Double.parseDouble(tokens[5]); 
      speed = temp * GeoTools.METERS_PER_HOUR_PER_KNOT / 1000;
      heading = Double.parseDouble(tokens[1]);
    }
    catch (NumberFormatException nfe) {
      ExtLogger.handle(nfe, false, "GPVTG Error parsing speed/heading data");
    }
  }
  
  /**
   * Process the GPGSA GPS command.
   * @param tokens the tokens of the command
   */
  private void handleGPGSA(String[] tokens) {
    try {
      if (tokens[2].equals("1")) // no fix
        return;
      
      numFixSatellites = 0;
      int count = 0;
      for (int i=0; i<12; i++)
        if (tokens[3+i].trim().length() > 0)
          fixSatellites[count++] = Integer.parseInt(tokens[3+i]);
      numFixSatellites = count;
    }
    catch (Exception ex) {
      ExtLogger.handle(ex, false, "GPGSA Error parsing fix satellites data");
    }
  }
  
  /**
   * Process the GPGSV GPS command.
   * @param tokens the tokens of the command
   */
  private void handleGPGSV(String[] tokens) {
    try {
      final int numSentence = Integer.parseInt(tokens[2]);
      // first sentence, delete stored data
      if (numSentence == 1) {
        numSatellites = 0;
        numSatellites = Integer.parseInt(tokens[3]);
        satelliteData = new SatelliteData[numSatellites];
      }
      
      int start = (numSentence-1)*4;
      for (int i=0; i<4; i++) {
        // no token for current satellite
        if (tokens.length <= i*4+5)
          break;
        // current satellite outside array
        if (i+start >= numSatellites)
          break;
        int satNum;
        try {
          satNum = Integer.parseInt(tokens[i*4+4]);
        }
        catch (Exception ex) {
          continue;
        }
        double satElev = 0;
        try {
          satElev = Double.parseDouble(tokens[i*4+5]);
        }
        catch (Exception ex) {
        }
        double satAz = 0;
        try {
          satAz = Double.parseDouble(tokens[i*4+6]); 
        }
        catch (Exception ex) {
        }
        double satSNR = 0;
        try {
          satSNR = Double.parseDouble(tokens[i*4+7]);
        }
        catch (Exception ex) {
        }
        satelliteData[i+start] = new SatelliteData(satNum, satElev, satAz, satSNR);
      }
    }
    catch (Exception ex) {
      ExtLogger.handle(ex, false, "GPGSV Error parsing satellite data");
    }
  }
  
  /**
   * Get the longitude and latitude.
   * @return the lon/lat info as a GPoint
   */
  public synchronized WgsPoint getPoint() {
    // memorize lon & lat if they are initialized
    if (lon != Double.NaN)
      oldLon = lon;
    if (lat != Double.NaN)
      oldLat = lat;
    
    // if lon & lat were never initialized, return null
    if (oldLat == Double.NaN)
      return null;
    
    // return lon & lat, zoom ignored
    return new WgsPoint(oldLon, oldLat);
  }
    
  /**
   * Read a line from a GPS input stream, new version (thanks Anders Borg)
   * 
   * @return the line read
   */
  protected String readLineV2() {
    
    int c;
    StringBuffer sb = new StringBuffer();

    try {
      while (true) {
        if (bufpos == 0) {
          buflen = is.read(buf, 0, buf.length);
          if (buflen == 0)
            Thread.sleep(VERY_SHORT_SLEEP_TIME);
        }

        if (buflen > 0) {
          c = buf[bufpos++];
          if (bufpos >= buflen)
            bufpos = 0;
          if (c == -1 || c == '\0' || c == '\n')
            break;
          if (c != '\r')
            sb.append((char) c);
        }
      }

      return sb.toString();
    }
    // I/O exception or interrupted exception
    catch (Exception ex) {
      ExtLogger.handle(ex, false, "Error receiving data");
      // disconnect if an error occurs
      disconnect();
      return "";
    }
  }
  
  /**
   * Read a line from a GPS input stream, old version.
   * 
   * @return the line read
   */
  protected String readLineV1() {
    try {
      // always sleep here
      Thread.sleep(SHORT_SLEEP_TIME_2);
      StringBuffer sb = new StringBuffer();
      int c = 0;
      for (int i=0; ; i++) {
        c = is.read();
        // very short sleep only on S60
        if (c == -1 || c == '\0' || c == '\n')
          break;
        if (c != '\r')
          sb.append((char) c);
      }
      return sb.toString();
    }
    // I/O exception or interrupted exception
    catch (Exception ex) {
      ExtLogger.handle(ex, false, "Error receiving data");
      // disconnect if an error occurs
      disconnect();
      return "";
    }
  }
  
  /**
   * Read a line from a GPS input stream, fixed for Nokia 7610/6620/6630.
   * 
   * @return the line read
   */
  protected String readLineV3() {
    try {
      Thread.sleep(SHORT_SLEEP_TIME_1);
      StringBuffer sb = new StringBuffer();
      int c = 0;
      for (int i=0; ; i++) {
        while (is.available() == 0)
          Thread.sleep(VERY_SHORT_SLEEP_TIME);
        c = is.read();
        // very short sleep only on S60
        if (i % VERY_SHORT_SLEEP_INT == 0)
          Thread.sleep(VERY_SHORT_SLEEP_TIME);
        if (c == -1 || c == '\0' || c == '\n')
          break;
        if (c != '\r')
          sb.append((char) c);
      }
      return sb.toString();
    }
    // I/O exception or interrupted exception
    catch (Exception ex) {
      ExtLogger.handle(ex, false, "Error receiving data");
      // disconnect if an error occurs
      disconnect();
      return "";
    }
  }

  /**
   * Sleep for some time.
   */
  protected synchronized void doSleep() {
    try {
      wait(SLEEP_TIME);
    }
    catch (InterruptedException ie) {
    }
  }
  
  /**
   * Synchronized getter for active.
   * 
   * @return the value of active
   */
  protected synchronized boolean isActive() {
    return active;
  }

  /**
   * Main thread method.
   * 
   * @see java.lang.Runnable#run()
   */
  public void run() {
    started = true;
    boolean firstTime = true;
    
    while (!isQuitting()) {
      hasFix = false;
      currentFix = false;
      if (!isActive()) {
        doSleep();
        continue;
      }
        
      while (!isConnected() && !isQuitting()) {
        if (!firstTime)
          doSleep();
        else
          firstTime = false;
        
        // try connecting
        connect();
      }

      // run only if connected
      while (isConnected() && !isQuitting()) {
        // read a line from the GPS device and trim it
        /* String line = (Props.gpsReadLine == 0) ? readLineV1() :
                      (Props.gpsReadLine == 1) ? readLineV2() :
                      readLineV3(); */
        String line = readLineV2();
        // NMEA debugging
        // Logger.debug("NMEA data: " + line);
        // parse the line
        parse(line);
      }
      
      // we're here if we get disconnected, but we are not quitting
      // resume the loop, wait to be again connected
    }
  }
}
